package org.example.domain.model.service;

import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.github.pagehelper.PageInfo;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.example.domain.model.config.UrlShortenerPathNotFoundConfig;
import org.example.domain.model.entity.url.UrlShortener;
import org.example.domain.model.entity.url.errorCode.UrlShortenerErrorCode;
import org.example.domain.model.entity.url.req.UrlShortenerCreateReq;
import org.example.domain.model.entity.url.req.UrlShortenerSelectReq;
import org.example.domain.model.entity.url.req.UrlShortenerUpdateReq;
import org.example.domain.model.entity.url.resp.QueryGroupByCountResp;
import org.example.domain.model.entity.url.resp.UrlShortenerCreateResp;
import org.example.domain.model.entity.url.vo.UrlShortenerStatsRecordVo;
import org.example.domain.model.entity.url.vo.UrlShortenerUpdateVo;
import org.example.domain.model.entity.url.vo.UrlShortenerVo;
import org.example.domain.model.util.HashUtil;
import org.example.domain.model.util.UrlShortenerUtil;
import org.example.domain.repository.UrlShortenerCorrelationGroupMapperRepository;
import org.example.domain.repository.UrlShortenerRepository;
import org.example.infrastructure.repository.RedisService;
import org.example.infrastructure.repository.orm.messageQueue.producer.UrlShortenerCoreStatsSaveProducer;
import org.example.infrastructure.repository.orm.redis.config.constant.RedisCacheConstant;
import org.example.utils.SnowFlake;
import org.example.web.exception.ServerException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

@Component
@Slf4j
@RequiredArgsConstructor
public class UrlShortenerDomainService {
    private final UrlShortenerRepository urlShortenerRepository;
    private final UrlShortenerCorrelationGroupMapperRepository urlShortenerCorrelationGroupMapperRepository;
    private final RBloomFilter<String> urlShortenerCacheBloomFilter;
    private final RedisService redisService;
    private final RedissonClient redissonClient;
    private final UrlShortenerCoreStatsSaveProducer urlShortenerCoreStatsSaveProducer;
    private final UrlShortenerPathNotFoundConfig urlShortenerPathNotFoundConfig;




    @Transactional(rollbackFor = Exception.class)
    public UrlShortenerCreateResp createUrlShortener(UrlShortenerCreateReq urlshortenerCreateReq) {
        final String domainName = urlshortenerCreateReq.getDomainName();
        final String originUrl = urlshortenerCreateReq.getOriginUrl();
        String shortLinkSuffix = generateSuffix(domainName, originUrl);
        String fullShortUrl = StrBuilder.create(urlshortenerCreateReq.getDomainName())
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        UrlShortener urlShortener = UrlShortener.builder()
                .domain(domainName)
                .originUrl(originUrl)
                .gid(urlshortenerCreateReq.getGid())
                .createdType(urlshortenerCreateReq.getCreatedType())
                .validDateType(urlshortenerCreateReq.getValidDateType())
                .validDate(urlshortenerCreateReq.getValidDate())
                .description(urlshortenerCreateReq.getDescription())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .fullShortUrl(fullShortUrl)
                .favicon(getFavicon(urlshortenerCreateReq.getOriginUrl())).build();

        try {
            urlShortenerRepository.insertUrlShortener(urlShortener);
            urlShortenerCorrelationGroupMapperRepository.insertUrlShortenerCorrelationGroup(fullShortUrl, urlshortenerCreateReq.getGid());
        } catch (DuplicateKeyException e) {
            //若数据库新增失败，可能数据库本身已存在数据，则布隆过滤器也新增，保证数据一致性
            // 首先判断是否存在布隆过滤器，如果不存在直接新增
            if (!urlShortenerCacheBloomFilter.contains(fullShortUrl)) {
                urlShortenerCacheBloomFilter.add(fullShortUrl);
            }
            throw new ServerException(String.format("短链接：%s 生成重复", fullShortUrl));
        }
        //跳转链接转存redis
        redisService.setCacheObject(RedisCacheConstant.SKIP_SHORT_LINK_KEY + fullShortUrl, originUrl,
                UrlShortenerUtil.getLinkCacheValidTime(urlShortener.getValidDate()),
                TimeUnit.MILLISECONDS);
        urlShortenerCacheBloomFilter.add(fullShortUrl);
        fullShortUrl = StrBuilder.create(urlshortenerCreateReq.getDomainName())
                .append(StringUtils.isNotBlank(urlShortenerPathNotFoundConfig.getContextPath()) ? urlShortenerPathNotFoundConfig.getContextPath() : "")
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        return UrlShortenerCreateResp.builder()
                .originUrl(originUrl)
                .fullShortUrl(fullShortUrl)
                .build();
    }

    /***
     *  原始链接以 MurmurHash 生成短链接
     * @param domainName    域名
     * @param originUrl     原始链接
     * @return
     */
    private String generateSuffix(String domainName, String originUrl) {
        int customGenerateCount = 0;
        String shorUri;
        SnowFlake snowFlake = new SnowFlake();
        while (true) {
            if (customGenerateCount >= 10) {
                throw new ServerException(UrlShortenerErrorCode.URL_SHORTENER_GENERATED_FREQUENTLY);
            }
            shorUri = HashUtil.hashToBase62(originUrl);
            if (!urlShortenerCacheBloomFilter.contains(domainName + "/" + shorUri)) {
                break;
            }
            originUrl += snowFlake.nextId();
            customGenerateCount++;
        }
        return shorUri;
    }

    private String getFavicon(String url) {
        try {
            URL targetUrl = new URL(url);
            HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
            connection.setRequestMethod("GET");
            connection.connect();
            int responseCode = connection.getResponseCode();
            if (HttpURLConnection.HTTP_OK == responseCode) {
                Document document = Jsoup.connect(url).get();
                Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
                if (faviconLink != null) {
                    return faviconLink.attr("abs:href");
                }
            }
        } catch (IOException e) {
            log.error("Error connecting to{}", e.getMessage());
            throw new RuntimeException(e);
        }
        return null;
    }


    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        String scheme = request.getScheme();
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort()).filter(port -> ObjectUtils.notEqual("80", port)).map(String::valueOf).map(port -> ":" + port).orElse("");
        String fullShortUrl = scheme + "://" +serverName + serverPort + "/" + shortUri;
        String originalLink = redisService.getCacheObject(RedisCacheConstant.SKIP_SHORT_LINK_KEY + fullShortUrl);
        if (StringUtils.isNotEmpty(originalLink)) {
            this.urlShortenerStats(this.buildUrlShortenerStatsRecordAndSetUser(fullShortUrl, request, response));
            sendRedirect(response, originalLink);
            return;
        }

        boolean contains = urlShortenerCacheBloomFilter.contains(fullShortUrl);
        if (!contains) {
            sendRedirect(response, urlShortenerPathNotFoundConfig.getUrlShortenerPathNotFound());
            return;
        }

        //如果redis不存在, 但是布隆中存在, 可能是缓存已过期, 也有可能是数据不一致导致的, 这个时间加锁查下DB
        RLock lock = redissonClient.getLock(RedisCacheConstant.LOCK_SKIP_SHORT_LINK_KEY + fullShortUrl);
        lock.lock();
        try {
            //在查一遍，防止加锁前已加入缓存
            originalLink = redisService.getCacheObject(RedisCacheConstant.SKIP_SHORT_LINK_KEY + fullShortUrl);
            if (StringUtils.isNotEmpty(originalLink)) {
                this.urlShortenerStats(this.buildUrlShortenerStatsRecordAndSetUser(fullShortUrl, request, response));
                sendRedirect(response, originalLink);
                return;
            }

            Optional<UrlShortenerVo> optional = urlShortenerRepository.selectUrlShortener(fullShortUrl);
            boolean isEmptyOrExpired = optional.isEmpty() || (optional.get().getValidDate() != null && optional.get().getValidDate().before(new Date()));
            if (isEmptyOrExpired) {
                redisService.setCacheObject(RedisCacheConstant.SKIP_SHORT_LINK_NULL_KEY + fullShortUrl, "empty", 24L, TimeUnit.HOURS);
                sendRedirect(response, urlShortenerPathNotFoundConfig.getUrlShortenerPathNotFound());
                return;
            }

            redisService.setCacheObject(
                    RedisCacheConstant.SKIP_SHORT_LINK_KEY + fullShortUrl,
                    optional.get().getOriginUrl(),
                    optional.get().acquireCacheValidTime(),
                    TimeUnit.MILLISECONDS);
            this.urlShortenerStats(this.buildUrlShortenerStatsRecordAndSetUser(fullShortUrl, request, response));
            sendRedirect(response, optional.get().getOriginUrl());
        } catch (Exception e) {
            throw new ServerException(UrlShortenerErrorCode.URL_SHORTENER_SKIP_EXCEPTION);
        } finally {
            lock.unlock();
        }


    }

    private void sendRedirect(ServletResponse response, String url) {
        try {
            ((HttpServletResponse) response).sendRedirect(url);
        } catch (IOException e) {
            throw new ServerException(UrlShortenerErrorCode.URL_SHORTENER_SKIP_EXCEPTION);
        }
    }


    @Transactional(rollbackFor = Exception.class)
    public void updateUrlShortener(UrlShortenerUpdateReq urlShortenerUpdateReq) {
        Optional<UrlShortenerVo> optional = urlShortenerRepository.selectUrlShortener(urlShortenerUpdateReq.getOldFullShortUrl());
        if (optional.isEmpty()) {
            throw new ServerException(UrlShortenerErrorCode.URL_SHORTENER_RECORD_DOES_NOT_EXIST);
        }
        UrlShortenerVo urlShortenerVo = optional.get();
        // TODO 暂时不确定是否判断有修改的部分
//        if (urlShortenerVo.getGid().equals(urlShortenerUpdateReq.getGid()) && urlShortenerVo.getOriginUrl().equals(urlShortenerUpdateReq.getOriginUrl())) {
//            throw new ServerException(UrlShortenerErrorCode.URL_SHORTENER_PLEASE_MAKE_CHANGES_IN_SUBMISSION);
//        }
        // 没有换组
        if (StringUtils.equals(urlShortenerVo.getGid(), urlShortenerUpdateReq.getGid())){
            UrlShortenerUpdateVo urlShortenerUpdateVo = new UrlShortenerUpdateVo();
            urlShortenerUpdateVo.setValidDateType(urlShortenerUpdateReq.getValidDateType());
            urlShortenerUpdateVo.setValidDate(urlShortenerUpdateReq.getValidDate());
            urlShortenerUpdateVo.setDescription(urlShortenerUpdateReq.getDescription());
            if (!urlShortenerVo.getOriginUrl().equals(urlShortenerUpdateReq.getNewOriginUrl())){
                String shortLinkSuffix = generateSuffix(urlShortenerVo.getDomain(), urlShortenerUpdateReq.getNewOriginUrl());
                String newFullShortUrl = StrBuilder.create(urlShortenerVo.getDomain())
                        .append("/")
                        .append(shortLinkSuffix).toString();
                urlShortenerUpdateVo.setNewOriginUrl(urlShortenerUpdateReq.getNewOriginUrl());
                urlShortenerUpdateVo.setNewFullShortUrl(newFullShortUrl);
                urlShortenerUpdateVo.setShortUri(shortLinkSuffix);
                //跳转链接转存redis
                redisService.setCacheObject(RedisCacheConstant.SKIP_SHORT_LINK_KEY + newFullShortUrl, urlShortenerUpdateReq.getNewOriginUrl(),
                        UrlShortenerUtil.getLinkCacheValidTime(urlShortenerUpdateReq.getValidDate()),
                        TimeUnit.MILLISECONDS);
                urlShortenerCacheBloomFilter.add(newFullShortUrl);
                urlShortenerCorrelationGroupMapperRepository.deleteByFullShortUrlAndGid(urlShortenerVo.getFullShortUrl(), urlShortenerVo.getGid());
                urlShortenerCorrelationGroupMapperRepository.insertUrlShortenerCorrelationGroup(urlShortenerUpdateVo.getNewFullShortUrl(), urlShortenerUpdateReq.getGid());

            }
            urlShortenerRepository.updateUrlShortEnerByFullShortUrl(urlShortenerUpdateVo, urlShortenerUpdateReq.getOldFullShortUrl(), urlShortenerVo.getGid());
        }else {
            // 已换组
            UrlShortenerUpdateVo urlShortenerUpdateVo = new UrlShortenerUpdateVo();
            urlShortenerUpdateVo.setDeleteIdentity(false);
            urlShortenerUpdateVo.setDelTime(new Date());
            urlShortenerRepository.updateUrlShortEnerByFullShortUrl(urlShortenerUpdateVo, urlShortenerUpdateReq.getOldFullShortUrl(), urlShortenerUpdateReq.getOriginGid());
            UrlShortener urlShortener = UrlShortener
                    .builder()
                    .domain(urlShortenerVo.getDomain())
                    .gid(urlShortenerUpdateReq.getGid())
                    .createdType(urlShortenerUpdateReq.getValidDateType())
                    .validDateType(urlShortenerUpdateReq.getValidDateType())
                    .validDate(urlShortenerUpdateReq.getValidDate())
                    .description(urlShortenerUpdateReq.getDescription())
                    .enableStatus(0)
                    .totalPv(0)
                    .totalUv(0)
                    .totalUip(0)
                    .build();
            if (!urlShortenerVo.getOriginUrl().equals(urlShortenerUpdateReq.getNewOriginUrl())){
                String shortLinkSuffix = generateSuffix(urlShortenerVo.getDomain(), urlShortenerUpdateReq.getNewOriginUrl());
                String newFullShortUrl = StrBuilder.create(urlShortenerVo.getDomain())
                        .append("/")
                        .append(shortLinkSuffix).toString();
                urlShortenerUpdateVo.setNewOriginUrl(urlShortenerUpdateReq.getNewOriginUrl());
                urlShortenerUpdateVo.setNewFullShortUrl(newFullShortUrl);
                urlShortenerUpdateVo.setShortUri(shortLinkSuffix);
                //跳转链接转存redis
                redisService.setCacheObject(RedisCacheConstant.SKIP_SHORT_LINK_KEY + newFullShortUrl, urlShortenerUpdateReq.getNewOriginUrl(),
                        UrlShortenerUtil.getLinkCacheValidTime(urlShortenerUpdateReq.getValidDate()),
                        TimeUnit.MILLISECONDS);
                urlShortenerCacheBloomFilter.add(newFullShortUrl);
            }else {
                urlShortener.setOriginUrl(urlShortenerVo.getOriginUrl());
                urlShortener.setShortUri(urlShortenerVo.getShortUri());
                urlShortener.setFullShortUrl(urlShortenerVo.getFullShortUrl());
                urlShortener.setFavicon(getFavicon(urlShortenerUpdateReq.getOriginUrl()));
                urlShortenerUpdateVo.setNewFullShortUrl(urlShortenerVo.getFullShortUrl());
            }
            try {
                urlShortenerRepository.insertUrlShortener(urlShortener);
                urlShortenerCorrelationGroupMapperRepository.deleteByFullShortUrlAndGid(urlShortenerVo.getFullShortUrl(), urlShortenerVo.getGid());
                urlShortenerCorrelationGroupMapperRepository.insertUrlShortenerCorrelationGroup(urlShortenerUpdateVo.getNewFullShortUrl(), urlShortenerUpdateReq.getGid());
            } catch (DuplicateKeyException e) {
                //若数据库新增失败，可能数据库本身已存在数据，则布隆过滤器也新增，保证数据一致性
                // 首先判断是否存在布隆过滤器，如果不存在直接新增
                if (!urlShortenerCacheBloomFilter.contains(urlShortener.getFullShortUrl())) {
                    urlShortenerCacheBloomFilter.add(urlShortener.getFullShortUrl());
                }
                throw new ServerException(String.format("短链接更新：%s 生成重复", urlShortener.getFullShortUrl()));
            }
        }

        if (!Objects.equals(urlShortenerVo.getValidDateType(), urlShortenerUpdateReq.getValidDateType())
                || !Objects.equals(urlShortenerVo.getValidDate(), urlShortenerUpdateReq.getValidDate())
                || !Objects.equals(urlShortenerVo.getOriginUrl(), urlShortenerUpdateReq.getNewOriginUrl())) {
            redisService.deleteObject(RedisCacheConstant.SKIP_SHORT_LINK_KEY + urlShortenerVo.getFullShortUrl());
            Date currentDate = new Date();
            if (urlShortenerVo.getValidDate() != null && urlShortenerVo.getValidDate().before(currentDate)) {
                if (Objects.equals(urlShortenerUpdateReq.getValidDateType(), 0) || urlShortenerUpdateReq.getValidDate().after(currentDate)) {
                    redisService.deleteObject(RedisCacheConstant.SKIP_SHORT_LINK_NULL_KEY + urlShortenerVo.getFullShortUrl());
                }
            }
        }
    }

    public PageInfo<UrlShortener> selectByPage(UrlShortenerSelectReq urlShortenerSelectReq) {
        PageInfo<UrlShortener> urlShortenerIPage = urlShortenerRepository.selectByPage(urlShortenerSelectReq);

        return urlShortenerIPage.convert(item->{
            item.setDomain("http://" + item.getDomain());
            return item;
        });
    }


    public List<QueryGroupByCountResp> queryGroupByCount(List<String> gidList) {
        return urlShortenerRepository.queryGroupByCount(gidList);
    }

    private UrlShortenerStatsRecordVo buildUrlShortenerStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            redisService.addCacheSet(RedisCacheConstant.URL_SHORTENER_STATS_UV_KEY + fullShortUrl, uv.get());
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdded = redisService.addCacheSet(RedisCacheConstant.URL_SHORTENER_STATS_UV_KEY + fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        String remoteAddr = UrlShortenerUtil.getActualIp(((HttpServletRequest) request));
        String os = UrlShortenerUtil.getOs(((HttpServletRequest) request));
        String browser = UrlShortenerUtil.getBrowser(((HttpServletRequest) request));
        String device = UrlShortenerUtil.getDevice(((HttpServletRequest) request));
        String network = UrlShortenerUtil.getNetwork(((HttpServletRequest) request));
        Long uipAdded = redisService.addCacheSet(RedisCacheConstant.URL_SHORTENER_STATS_UV_KEY + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return UrlShortenerStatsRecordVo.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .currentDate(new Date())
                .build();
    }

    public void urlShortenerStats(UrlShortenerStatsRecordVo statsRecord) {
        Map<String, String> producerMap = new HashMap<>();
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        urlShortenerCoreStatsSaveProducer.send(producerMap);
    }
}


